package net.aiion.weave.impl.cards.xml;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Stack;

import net.aiion.weave.asm.Label;
import net.aiion.weave.asm.Opcodes;
import net.aiion.weave.asm.Type;
import net.aiion.weave.spi.bytecode.BytecodeGenerator;
import net.aiion.weave.spi.cards.CardContext;
import net.aiion.weave.spi.cards.CardException;
import net.aiion.weave.spi.cards.CardXmlNames;
import net.aiion.weave.spi.cards.events.CardEvent;
import net.aiion.weave.spi.cards.expressions.CardExpressionResolverService;
import net.aiion.weave.spi.xml.XmlContext;
import net.aiion.weave.spi.xml.XmlSink;
import net.aiion.weave.spi.xml.events.ElementEndXmlEvent;
import net.aiion.weave.spi.xml.events.ElementStartXmlEvent;
import net.aiion.weave.spi.xml.events.XmlEvent;

public class ForTransformation implements XmlSink
{
    private static final String PROP_INTERNAL_NAME = "internalName";

    private static final int INDEX_CARD_CONTEXT = 1;
    private static final int INDEX_IN_VALUE = 2;
    private static final int INDEX_IN_VALUE_COLLECTION = 3;
    private static final int INDEX_IN_VALUE_COLLECTION_VALUE = 4;
    private static final int INDEX_IN_VALUE_COLLECTION_ITERATOR = 5;
    private static final int INDEX_IN_VALUE_ARRAY_LIST = 3;
    private static final int INDEX_IN_VALUE_ARRAY_LENGTH = 4;
    private static final int INDEX_IN_VALUE_ARRAY_INDEX = 5;

    private static final String OBJECT_INTERNAL_NAME = "java/lang/Object";

    private static final Type COLLECTION_TYPE = Type.getType(Collection.class);
    private static final String COLLECTION_INTERNAL_NAME = COLLECTION_TYPE.getInternalName();

    private static final String ITERATOR_INTERNAL_NAME = "java/util/Iterator";

    private static final String CARD_EXPRESSION_RESOLVER_SERVICE_FIELD_NAME = "expressionResolver$ForXmlSink";

    private static final String FOR_ATTRIBUTE_EACH_NAME = "each";
    private static final String FOR_ATTRIBUTE_IN_NAME = "in";

    private BytecodeGenerator bytecodeGenerator;

    private Stack<LoopState> loopStates;

    public ForTransformation() {
        loopStates = new Stack<>();
    }

    @Override
    public void handleXmlEvent(final XmlContext aXmlContext) {
        bytecodeGenerator = aXmlContext.getProperty("bytecodeGenerator", BytecodeGenerator.class);
        final XmlEvent xmlEvent = aXmlContext.getXmlEvent();
        switch (xmlEvent.getType()) {
        case CardEvent.CARD_CLASS_START:
            bytecodeGenerator.startField(Opcodes.ACC_PRIVATE, CARD_EXPRESSION_RESOLVER_SERVICE_FIELD_NAME,
                CardExpressionResolverService.class);
            bytecodeGenerator.endField();
            aXmlContext.forwardXmlEvent(xmlEvent);
            break;
        case CardEvent.CARD_PROCESS_CARD_METHOD_START:
            bytecodeGenerator.loadReference(0);
            bytecodeGenerator.loadReference(INDEX_CARD_CONTEXT);
            bytecodeGenerator.invokeInterface(CardContext.class, "getExpressionResolverService",
                CardExpressionResolverService.class);
            bytecodeGenerator.storeField(aXmlContext.getProperty(PROP_INTERNAL_NAME, String.class),
                CARD_EXPRESSION_RESOLVER_SERVICE_FIELD_NAME, CardExpressionResolverService.class);
            aXmlContext.forwardXmlEvent(xmlEvent);
            break;
        case XmlEvent.START_ELEMENT:
            handleStartElementXmlEvent(aXmlContext, xmlEvent.asElementStart());
            break;
        case XmlEvent.END_ELEMENT:
            handleEndElementXmlEvent(aXmlContext, xmlEvent.asElementEnd());
            break;
        default:
            aXmlContext.forwardXmlEvent(xmlEvent);
            break;
        }
    }

    private void handleStartElementXmlEvent(final XmlContext aXmlContext, final ElementStartXmlEvent aXmlStartElement) {
        if (!CardXmlNames.FOR.equals(aXmlStartElement.getName())) {
            aXmlContext.forwardXmlEvent(aXmlStartElement);
            return;
        }

        final String eachName = aXmlStartElement.getAttribute(FOR_ATTRIBUTE_EACH_NAME).getValue();

        final Label testEvaluatedArrayLabel = new Label();
        final Label initializeLoopLabel = new Label();
        final Label unsupportedEvaluatedLabel = new Label();
        final Label storeArrayLengthLabel = new Label();
        final Label storeNextArrayValueInCollectionLabel = new Label();
        final Label testIterationLabel = new Label();
        final Label executeIterationLabel = new Label();

        loopStates.push(new LoopState(eachName, testIterationLabel, executeIterationLabel));

        // <<INDEX_IN_VALUE>> = <<INDEX_CARD_CONTEXT>>.getExpressionResolverService().readExpression(<<in-attr-value>>)
        bytecodeGenerator.loadReference(0);
        bytecodeGenerator.loadField(aXmlContext.getProperty(PROP_INTERNAL_NAME, String.class),
            CARD_EXPRESSION_RESOLVER_SERVICE_FIELD_NAME, CardExpressionResolverService.class);
        bytecodeGenerator.pushConstant(aXmlStartElement.getAttribute(FOR_ATTRIBUTE_IN_NAME).getValue());
        bytecodeGenerator.invokeInterface(CardExpressionResolverService.class, "readExpression", Object.class,
            String.class);
        bytecodeGenerator.storeReference(INDEX_IN_VALUE);

        // <<INDEX_IN_VALUE>> instanceof Collection
        // IFEQ <<testEvaluatedArrayLabel>>
        // <<INDEX_IN_VALUE_COLLECTION>> = (Collection) <<INDEX_IN_VALUE>>
        bytecodeGenerator.loadReference(INDEX_IN_VALUE);
        bytecodeGenerator.instanceOf(Collection.class);
        bytecodeGenerator.jumpEquals(testEvaluatedArrayLabel);

        bytecodeGenerator.loadReference(INDEX_IN_VALUE);
        bytecodeGenerator.castChecked(Collection.class);
        bytecodeGenerator.storeReference(INDEX_IN_VALUE_COLLECTION);

        bytecodeGenerator.gotoLabel(initializeLoopLabel);
        bytecodeGenerator.markLabel(testEvaluatedArrayLabel);
        bytecodeGenerator.markFrameAppend(2, new Object[] {
            OBJECT_INTERNAL_NAME, COLLECTION_INTERNAL_NAME
        }, 0, null);

        // <<INDEX_IN_VALUE>>.getClass().isArray()
        // IFEQ <<unsupportedEvaluatedLabel>>
        // <<indexXmlCardValueEvaluatedArrayList>> = new ArrayList()
        // <<indexXmlCardValueEvaluatedArrayLength>> = Array.getLength(<<indexXmlCardValueEvaluated>>)
        // <<indexXmlCardValueEvaluatedArrayIndex>> = 0
        bytecodeGenerator.loadReference(INDEX_IN_VALUE);
        bytecodeGenerator.invokeVirtual(Object.class, "getClass", Class.class);
        bytecodeGenerator.invokeVirtual(Class.class, "isArray", boolean.class);
        bytecodeGenerator.jumpEquals(unsupportedEvaluatedLabel);

        bytecodeGenerator.newReference(ArrayList.class);
        bytecodeGenerator.duplicateReference();
        bytecodeGenerator.invokeConstructor(ArrayList.class);
        bytecodeGenerator.storeReference(INDEX_IN_VALUE_ARRAY_LIST);

        bytecodeGenerator.loadReference(INDEX_IN_VALUE);
        bytecodeGenerator.invokeStatic(Array.class, "getLength", int.class, Object.class);
        bytecodeGenerator.storeInteger(INDEX_IN_VALUE_ARRAY_LENGTH);

        bytecodeGenerator.pushInteger(0);
        bytecodeGenerator.storeInteger(INDEX_IN_VALUE_ARRAY_INDEX);

        bytecodeGenerator.gotoLabel(storeArrayLengthLabel);
        bytecodeGenerator.markLabel(storeNextArrayValueInCollectionLabel);
        bytecodeGenerator.markFrameAppend(2, new Object[] {
            Opcodes.INTEGER, Opcodes.INTEGER
        }, 0, null);

        bytecodeGenerator.loadReference(INDEX_IN_VALUE_ARRAY_LIST);
        bytecodeGenerator.loadReference(INDEX_IN_VALUE);
        bytecodeGenerator.loadInteger(INDEX_IN_VALUE_ARRAY_INDEX);
        bytecodeGenerator.invokeStatic(Array.class, "get", Object.class, Object.class, int.class);
        bytecodeGenerator.invokeInterface(Collection.class, "add", boolean.class, Object.class);
        bytecodeGenerator.popReference();

        bytecodeGenerator.incrementInteger(INDEX_IN_VALUE_ARRAY_INDEX);

        bytecodeGenerator.markLabel(storeArrayLengthLabel);
        bytecodeGenerator.markFrameSame();
        bytecodeGenerator.loadInteger(INDEX_IN_VALUE_ARRAY_INDEX);
        bytecodeGenerator.loadInteger(INDEX_IN_VALUE_ARRAY_LENGTH);
        bytecodeGenerator.jumpIntegerLessThan(storeNextArrayValueInCollectionLabel);
        bytecodeGenerator.gotoLabel(initializeLoopLabel);

        bytecodeGenerator.markLabel(unsupportedEvaluatedLabel);
        bytecodeGenerator.markFrameChop(1, null, 0, null);
        // throw new CardException("blah blah")
        bytecodeGenerator.newReference(CardException.class);
        bytecodeGenerator.duplicateReference();
        bytecodeGenerator.pushConstant(String.format("Value '%s' is not an array or a collection.",
            aXmlStartElement.getAttributeValue(FOR_ATTRIBUTE_IN_NAME)));
        bytecodeGenerator.invokeConstructor(CardException.class, String.class);
        bytecodeGenerator.throwReference();

        bytecodeGenerator.markLabel(initializeLoopLabel);
        bytecodeGenerator.markFrameSame();
        bytecodeGenerator.loadReference(INDEX_IN_VALUE_COLLECTION);
        bytecodeGenerator.invokeInterface(Collection.class, "iterator", Iterator.class);
        bytecodeGenerator.storeReference(INDEX_IN_VALUE_COLLECTION_ITERATOR);
        bytecodeGenerator.gotoLabel(testIterationLabel);

        bytecodeGenerator.markLabel(executeIterationLabel);
        bytecodeGenerator.markFrameFull(5, new Object[] {
            aXmlContext.getProperty(PROP_INTERNAL_NAME, String.class), OBJECT_INTERNAL_NAME, COLLECTION_INTERNAL_NAME,
            Opcodes.TOP, ITERATOR_INTERNAL_NAME
        }, 0, new Object[] {});

        bytecodeGenerator.loadReference(INDEX_IN_VALUE_COLLECTION_ITERATOR);
        bytecodeGenerator.invokeInterface(Iterator.class, "next", Object.class);
        bytecodeGenerator.storeReference(INDEX_IN_VALUE_COLLECTION_VALUE);

        // <<INDEX_CARD_CONTEXT>>.getExpressionResolverService().writeExpression(<<each-attr-value>>,
        // <<INDEX_IN_VALUE_COLLECTION_VALUE>>)
        bytecodeGenerator.loadReference(0);
        bytecodeGenerator.loadField(aXmlContext.getProperty(PROP_INTERNAL_NAME, String.class),
            CARD_EXPRESSION_RESOLVER_SERVICE_FIELD_NAME, CardExpressionResolverService.class);
        bytecodeGenerator.pushConstant(eachName);
        bytecodeGenerator.loadReference(INDEX_IN_VALUE_COLLECTION_VALUE);
        bytecodeGenerator.invokeInterface(CardExpressionResolverService.class, "writeExpression", void.class,
            String.class, Object.class);
    }

    private void handleEndElementXmlEvent(final XmlContext aXmlContext, final ElementEndXmlEvent aXmlEndElement) {
        if (!CardXmlNames.FOR.equals(aXmlEndElement.getName())) {
            aXmlContext.forwardXmlEvent(aXmlEndElement);
        }

        final LoopState loopState = loopStates.pop();

        bytecodeGenerator.markLabel(loopState.getTestIterationLabel());
        bytecodeGenerator.markFrameSame();
        bytecodeGenerator.loadReference(INDEX_IN_VALUE_COLLECTION_ITERATOR);
        bytecodeGenerator.invokeInterface(Iterator.class, "hasNext", boolean.class);
        bytecodeGenerator.jumpNotEquals(loopState.getExecuteIterationLabel());

        // <<INDEX_CARD_CONTEXT>>.getExpressionResolverService().writeExpression(<<each-attr-value>>, null)
        bytecodeGenerator.loadReference(0);
        bytecodeGenerator.loadField(aXmlContext.getProperty(PROP_INTERNAL_NAME, String.class),
            CARD_EXPRESSION_RESOLVER_SERVICE_FIELD_NAME, CardExpressionResolverService.class);
        bytecodeGenerator.pushConstant(loopState.getEachName());
        bytecodeGenerator.pushNull();
        bytecodeGenerator.invokeInterface(CardExpressionResolverService.class, "writeExpression", void.class,
            String.class, Object.class);
    }

    private static class LoopState
    {
        private String eachName;
        private Label testIterationLabel;
        private Label executeIterationLabel;

        public LoopState(final String aEachName, final Label aTestIterationLabel, final Label aExecuteIterationLabel) {
            eachName = aEachName;
            testIterationLabel = aTestIterationLabel;
            executeIterationLabel = aExecuteIterationLabel;
        }

        public String getEachName() {
            return eachName;
        }

        public Label getTestIterationLabel() {
            return testIterationLabel;
        }

        public Label getExecuteIterationLabel() {
            return executeIterationLabel;
        }
    }
}
